/*
 * Copyright 2002-2011 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cn.cloud.all.security.oauth2.client.token;

import cn.cloud.all.security.access.AccessDeniedException;
import cn.cloud.all.security.authentication.AnonymousAuthenticationToken;
import cn.cloud.all.security.authentication.exception.InsufficientAuthenticationException;
import cn.cloud.all.security.core.Authentication;
import cn.cloud.all.security.core.context.SecurityContextHolder;
import cn.cloud.all.security.oauth2.client.exception.OAuth2AccessDeniedException;
import cn.cloud.all.security.oauth2.client.exception.UserRedirectRequiredException;
import cn.cloud.all.security.oauth2.client.provider.AccessTokenProvider;
import cn.cloud.all.security.oauth2.client.provider.OAuth2AccessTokenSupport;
import cn.cloud.all.security.oauth2.common.DefaultOAuth2AccessToken;
import cn.cloud.all.security.oauth2.common.OAuth2AccessToken;
import cn.cloud.all.security.oauth2.common.OAuth2RefreshToken;
import cn.cloud.all.security.oauth2.common.exceptions.OAuth2Exception;
import cn.cloud.all.security.oauth2.resource.OAuth2ProtectedResourceDetails;

import java.util.Collections;
import java.util.List;

public class AccessTokenProviderChain extends OAuth2AccessTokenSupport implements AccessTokenProvider {

    private final List<AccessTokenProvider> chain;

    private ClientTokenServices clientTokenServices;

    public AccessTokenProviderChain(List<? extends AccessTokenProvider> chain) {
        this.chain = chain == null ? Collections.<AccessTokenProvider>emptyList() : Collections.unmodifiableList(chain);
    }

    public void setClientTokenServices(ClientTokenServices clientTokenServices) {
        this.clientTokenServices = clientTokenServices;
    }

    public boolean supportsResource(OAuth2ProtectedResourceDetails resource) {
        for (AccessTokenProvider tokenProvider : chain) {
            if (tokenProvider.supportsResource(resource)) {
                return true;
            }
        }
        return false;
    }

    public boolean supportsRefresh(OAuth2ProtectedResourceDetails resource) {
        for (AccessTokenProvider tokenProvider : chain) {
            if (tokenProvider.supportsRefresh(resource)) {
                return true;
            }
        }
        return false;
    }

    public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails resource, AccessTokenRequest request) throws UserRedirectRequiredException, AccessDeniedException {

        OAuth2AccessToken accessToken = null;
        OAuth2AccessToken existingToken = null;
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();

        if (auth instanceof AnonymousAuthenticationToken) {
            if (!resource.isClientOnly()) {
                throw new InsufficientAuthenticationException("Authentication is required to obtain an access token (anonymous not allowed)");
            }
        }

        if (resource.isClientOnly() || (auth != null && auth.isAuthenticated())) {
            existingToken = request.getExistingToken();
            if (existingToken == null && clientTokenServices != null) {
                existingToken = clientTokenServices.getAccessToken(resource, auth);
            }

            if (existingToken != null) {
                if (existingToken.isExpired()) {
                    if (clientTokenServices != null) {
                        clientTokenServices.removeAccessToken(resource, auth);
                    }
                    OAuth2RefreshToken refreshToken = existingToken.getRefreshToken();
                    if (refreshToken != null && !resource.isClientOnly()) {
                        accessToken = refreshAccessToken(resource, refreshToken, request);
                    }
                } else {
                    accessToken = existingToken;
                }
            }
        }
        // Give unauthenticated users a chance to get a token and be redirected

        if (accessToken == null) {
            // looks like we need to try to obtain a new token.
            accessToken = obtainNewAccessTokenInternal(resource, request);

            if (accessToken == null) {
                throw new IllegalStateException("An OAuth 2 access token must be obtained or an exception thrown.");
            }
        }

        if (clientTokenServices != null && (resource.isClientOnly() || auth != null && auth.isAuthenticated())) {
            clientTokenServices.saveAccessToken(resource, auth, accessToken);
        }

        return accessToken;
    }

    protected OAuth2AccessToken obtainNewAccessTokenInternal(OAuth2ProtectedResourceDetails details, AccessTokenRequest request) throws UserRedirectRequiredException, AccessDeniedException {
        if (request.isError()) {
            // there was an oauth error...
            throw OAuth2Exception.valueOf(request.toSingleValueMap());
        }

        for (AccessTokenProvider tokenProvider : chain) {
            if (tokenProvider.supportsResource(details)) {
                return tokenProvider.obtainAccessToken(details, request);
            }
        }

        throw new OAuth2AccessDeniedException("Unable to obtain a new access token for resource '" + details.getId() + "'. The provider manager is not configured to support it.", details);
    }

    public OAuth2AccessToken refreshAccessToken(OAuth2ProtectedResourceDetails resource, OAuth2RefreshToken refreshToken, AccessTokenRequest request) throws UserRedirectRequiredException {
        for (AccessTokenProvider tokenProvider : chain) {
            if (tokenProvider.supportsRefresh(resource)) {
                DefaultOAuth2AccessToken refreshedAccessToken = new DefaultOAuth2AccessToken(tokenProvider.refreshAccessToken(resource, refreshToken, request));
                if (refreshedAccessToken.getRefreshToken() == null) {
                    // Fixes gh-712
                    refreshedAccessToken.setRefreshToken(refreshToken);
                }
                return refreshedAccessToken;
            }
        }
        throw new OAuth2AccessDeniedException("Unable to obtain a new access token for resource '" + resource.getId() + "'. The provider manager is not configured to support it.", resource);
    }
}
